%%%----------------------------------------------------------------------
%%% File    : cus_ejabberd_c2s.erl
%%% Author  : chen.kangmin
%%%----------------------------------------------------------------------

-module(cus_c2s).
-author('chen.kangmin').


-behaviour(gen_server).

%% External exports
-export([start/2,
         socket_type/0
        ]
       ).

-export([init/1,
         start_link/2,
         code_change/3,
         handle_info/2,
         handle_call/3,
         handle_cast/2,
         terminate/2
        ]).


-include("logger.hrl").
-include("ejabberd.hrl").
-include("cus_pb.hrl").
-include("cus.hrl").


-record(state, {socket,
                encryptFlag = false,
                privateKey = undefined,
                secretKey = undefined,
                data = <<>>,
                size = 0,
                bound = undefined}).



-define(C2S_OPEN_TIMEOUT, 60000).
-define(C2S_HIBERNATE_TIMEOUT, 90000).


-define(CUS_ERROR, -1).
-define(CUS_LOGIN_TYPE, 1).
-define(CUS_LOGIN_RESP_TYPE, 2).
-define(CUS_MESSAGE_TYPE, 3).
-define(CUS_MESSAGE_ACK_TYPE, 4).
-define(CUS_PUSH_CONTENT_TYPE, 5).
-define(CUS_KICK_TYPE, 6).



%%%----------------------------------------------------------------------
%%% API
%%%----------------------------------------------------------------------
start(SockData, Opts) ->
    supervisor:start_child(cus_c2s_sup,
                           [SockData, Opts]).


start_link(SockData, Opts) ->
    gen_server:start_link(?MODULE, [SockData, Opts], []).


socket_type() ->
    raw.


%%----------------------------------------------------------------------
%% Func: init/1
%% Returns: {ok, StateName, StateData}          |
%%          {ok, StateName, StateData, Timeout} |
%%          ignore                              |
%%          {stop, StopReason}
%%----------------------------------------------------------------------
init([{_SockMod, Socket}, _Opts]) ->
    [EncryptFlag, PrivateKey] = cus_config:connect_encrypt(),
    State = #state{socket         = Socket,
                   encryptFlag    = EncryptFlag,
                   privateKey     = PrivateKey},
    activate_socket(State),
    {ok, State, ?C2S_OPEN_TIMEOUT}.
%%--------------------------------------------------------------------
%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} |
%%                                      {reply, Reply, State, Timeout} |
%%                                      {noreply, State} |
%%                                      {noreply, State, Timeout} |
%%                                      {stop, Reason, Reply, State} |
%%                                      {stop, Reason, State}
%% Description: Handling call messages
%%--------------------------------------------------------------------
handle_call(_Request, _From, State) ->
    Reply = ok,
    {reply, Reply, State}.

%%--------------------------------------------------------------------
%% Function: handle_cast(Msg, State) -> {noreply, State} |
%%                                      {noreply, State, Timeout} |
%%                                      {stop, Reason, State}
%% Description: Handling cast messages
%%--------------------------------------------------------------------
handle_cast(closed, State) ->
    {stop, normal, State};
                      
handle_cast(_Msg, State) ->
    {noreply, State}.

%%--------------------------------------------------------------------
%% Function: handle_info(Info, State) -> {noreply, State} |
%%                                       {noreply, State, Timeout} |
%%                                       {stop, Reason, State}
%% Description: Handling all non call/cast messages
%%--------------------------------------------------------------------
handle_info({router, Record}, State) ->
    Bin = cus_pb:encode(Record),
    RespBin = case State#state.encryptFlag of
                  true ->
                      cus_util:aes_cfb_128_encrypt(Bin, State#state.secretKey);
                  _ ->
                      Bin
              end,
    BoundBin = encode_bound(Record, size(RespBin)),
    gen_tcp:send(State#state.socket, <<BoundBin/binary, RespBin/binary>>),
    {noreply, State};

handle_info({tcp, _TCPSocket, Data}, State) ->
    OldBuff = State#state.data,
    OldSize = State#state.size,
    State1 = State#state{data = <<OldBuff/binary, Data/binary>>, size = OldSize + size(Data)},
    try
        State2 = parse_data(State1),
        activate_socket(State2),
        {noreply, State2, ?C2S_HIBERNATE_TIMEOUT}
    catch
        Type:Err ->
            ?ERROR_MSG("PARSE DATA ERROR, TYPE : ~p, ERROR : ~p ,  ~p ~n", [Type, Err, erlang:get_stacktrace()]),
            {stop, normal, State}
    end;
    
handle_info({tcp_closed, _TCPSocket}, State) ->
    {stop, normal, State};

handle_info({tcp_error, _TCPSocket, Reason}, State) ->
    case Reason of
    	timeout ->
    	    {noreply, State};
    	_ ->
    	    {stop, normal, State}
    end;
    
handle_info(timeout, State) ->
    proc_lib:hibernate(gen_server, enter_loop, [?MODULE, [], State]),
    {noreply, State};
    
handle_info(_Info, State) ->
    {noreply, State}.

%%--------------------------------------------------------------------
%% Function: terminate(Reason, State) -> void()
%% Description: This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any necessary
%% cleaning up. When it returns, the gen_server terminates with Reason.
%% The return value is ignored.
%%--------------------------------------------------------------------
terminate(_Reason, State) ->
catch gen_tcp:close(State#state.socket),
    cus_do:do_stop().

%%--------------------------------------------------------------------
%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState}
%% Description: Convert process state when code is changed
%%--------------------------------------------------------------------
code_change(_OldVsn, State, _Extra) ->
    {ok, State}.


activate_socket(#state{socket = Socket} ) ->
    inet:setopts(Socket, [{active, once}]),
    case inet:peername(Socket) of
        {error, _Reason} ->
            self() ! {tcp_closed, Socket};
        {ok, _} ->
            ok
    end.


parse_data(State) when State#state.encryptFlag =:= true andalso State#state.secretKey =:= undefined->
    Data = State#state.data,
    Size = State#state.size - 175,
    <<HandshakeBin:175/binary, Buff:Size/binary>> = Data,
    Handshake = cus_pb:decode_handshake(HandshakeBin),
    SecretKeyBin = base64:decode(Handshake#handshake.secretkey),
    SecretKey = cus_util:decrypt_private(SecretKeyBin, State#state.privateKey),
    Record = #handshakeresp{code = 0},
    Bin = cus_pb:encode(Record),
    gen_tcp:send(State#state.socket, Bin),
    State#state{data = Buff, size = Size, secretKey = SecretKey};
parse_data(State) when State#state.bound =:= undefined andalso State#state.size >= 10 ->
    Data = State#state.data,
    Size = State#state.size - 10,
    <<BoundBin:10/binary, Buff:Size/binary>> = Data,
    Bound = cus_pb:decode_bound(BoundBin),
    if
        Bound#bound.type =:= 0 ->   %%心跳协议
            parse_data(State#state{data = Buff, size = Size});
        true ->
            if
                Size >= Bound#bound.size ->
                    Size1 = Bound#bound.size,
                    Size2 = Size - Size1,
                    <<BusinessBin:Size1/binary, Buff1:Size2/binary>> = Buff,
                    ProtoBin = case State#state.encryptFlag of
                                   true ->
                                       cus_util:aes_cfb_128_decrypt(BusinessBin, State#state.secretKey);
                                   _ ->
                                       BusinessBin
                               end,
                    Record = decode_protocal(ProtoBin, Bound#bound.type),
                    exec(Record),
                    parse_data(State#state{data = Buff1, size = Size2, bound = undefined});
                true ->
                    State#state{data = Buff, size = Size, bound = Bound}
            end
    end;
parse_data(State) when State#state.bound =:= undefined ->
    State;
parse_data(State) ->
    Bound = State#state.bound,
    if
        State#state.size >= Bound#bound.size ->
            Data = State#state.data,
            Size1 = Bound#bound.size,
            Size = State#state.size - Size1,
            <<BusinessBin:Size1/binary, Buff:Size/binary>> = Data,
            ProtoBin = case State#state.encryptFlag of
                           true ->
                               cus_util:aes_cfb_128_decrypt(BusinessBin, State#state.secretKey);
                           _ ->
                               BusinessBin
                       end,
            Record = decode_protocal(ProtoBin, Bound#bound.type),
            exec(Record),
            parse_data(State#state{data = Buff, size = Size, bound = undefined});
        true ->
            State
    end.


decode_protocal(ProtoBin, ?CUS_LOGIN_TYPE) ->         %%登录协议
    cus_pb:decode_login(ProtoBin);
decode_protocal(ProtoBin, ?CUS_MESSAGE_TYPE) ->       %%消息协议
    cus_pb:decode_msg(ProtoBin);
decode_protocal(ProtoBin, ?CUS_MESSAGE_ACK_TYPE) ->   %%消息ack协议
    cus_pb:decode_messageack(ProtoBin);
decode_protocal(_ProtoBin, _) ->
    throw("UNKNOW PROTOCAL").


exec(Record) when is_record(Record, login) ->
    try
        cus_do:do_login(Record)
    catch
        throw:{CODE, MSG} ->
            ?ERROR_MSG("DO RECORD : ~p ERROR, CODE : ~p, MSG  ~p~n", [Record, CODE, MSG]),
            Pid = self(),
            Pid ! {router, #error{code = CODE, errormsg = MSG}};
        TYPE:ERR ->
            ?ERROR_MSG("DO RECORD : ~p UNKNOW ERROR, TYPE : ~p, ERR  ~p~n", [Record, TYPE, ERR]),
            Pid = self(),
            Pid ! {router, #error{code = "0000", errormsg = "UNKNOW ERROR"}}
    end;
exec(Record) when is_record(Record, msg) ->
    Login = get(login),
    Uid = get(uid),
    spawn(fun() -> cus_do:do_message(Login, Uid, Record) end);
exec(Record) when is_record(Record, messageack) ->
    Login = get(login),
    Uid = get(uid),
    spawn(fun() -> cus_do:do_message_ack(Login, Uid, Record) end);
exec(_Record) ->
    skip.


encode_bound(Record, Size) when is_record(Record, error) ->
    cus_pb:encode_bound(#bound{type = ?CUS_ERROR, size = Size});
encode_bound(Record, Size) when is_record(Record, loginresp) ->
    cus_pb:encode_bound(#bound{type = ?CUS_LOGIN_RESP_TYPE, size = Size});
encode_bound(Record, Size) when is_record(Record, msg) ->
    cus_pb:encode_bound(#bound{type = ?CUS_MESSAGE_TYPE, size = Size});
encode_bound(Record, Size) when is_record(Record, messageack) ->
    cus_pb:encode_bound(#bound{type = ?CUS_MESSAGE_ACK_TYPE, size = Size});
encode_bound(Record, Size) when is_record(Record, pushcontent) ->
    cus_pb:encode_bound(#bound{type = ?CUS_PUSH_CONTENT_TYPE, size = Size});
encode_bound(Record, Size) when is_record(Record, kick) ->
    cus_pb:encode_bound(#bound{type = ?CUS_KICK_TYPE, size = Size});
encode_bound(_Record, _Size) ->
    skip.